Come detto da @Edward una delle ottimizzazioni da fare è sicuramente il fatto di smettere di ciclare sul dizionario una volta generate n tuple nel risultato, ma un'altra cosa importante è non leggere l'intero file.
Spero di non fare una gaffe azzardando questa supposizione sul tuo programma, ma...
Se tutti i test ti vengono giusti ma vai in timeout immagino tu abbia letto l'intero file.
Cosa che ti permette sicuramente di fare un ciclo per i da 0 a alla lunghezza della stringa del file, e all'interno farne un altro per j da a a b in modo da fare uno slice a partire da i, fino a i+j.
Questo metodo per quanto chiaro e concettualmente funzionante ti costringe a creare delle stringhe enormi, la cui gestione (es: slice) in python è decisamente lenta.
Un metodo alternativo può essere leggere solo b caratteri invece di leggere tutta la stringa.
Tenendo un buffer di b caratteri, puoi fare un ciclo per j da a fino a b per fare uno slice poi da 0 a j, questo ti permette di trovare le sottosequenze (a-b) tagliando una sequenza di massimo b caratteri invece di farlo tagliando l'intera stringa.
Ma una volta trovate le sottosequenze della prima sequenza di b caratteri come vai avanti?
Semplicemente togli il primo carattere dal buffer e aggiungi un altro in coda letto dal file, e a quel punto fai di nuovo il ciclo.